<?php

namespace pmvc\ioc;

use Exception;

/**
 * An {@link ObjectFactory} that uses XML
 * configuration files for configuration.
 *
 */
class XmlObjectFactory
	extends DescriptorObjectFactory {

	/**
	 * Creates the {@link XmlObjectFactory} using the
	 * given xml files.
	 * @param $xmlFiles the files to add
	 */
	public function __construct($xmlFiles=Array(), ObjectFactory $parent=NULL) {
		parent::__construct($parent);
		foreach ($xmlFiles as $xmlFile) {
			$this->addXmlFile($xmlFile);
		}
	}

	/**
	 * Adds an xml object configuration file to the
	 * {@link ObjectFactory}.
	 * @param string $xmlFile the file to add
	 */
	public function addXmlFile($xmlFile) {
		
		// load the configuration file
		$xml = simplexml_load_file($xmlFile);
		if ($xml===FALSE) {
			throw new Exception(
				"Unable to parse configuration file: ".$xmlFile);
		}

		// load properties
		foreach ($xml->xpath("/objects/*") as $objectEl) {

			// <import file="" />
			if ($objectEl->getName()=="import") {
				$this->assimilateImport($objectEl, $xmlFile);

			// <properties ...
			} else if ($objectEl->getName()=="properties") {
				$this->assimilateProperties($objectEl, $xmlFile);

			// <object ...
			} else if ($objectEl->getName()=="object") {
				$this->assimilateObject($objectEl, $xmlFile);
			}
		}
	}

	private function assimilateImport($importEl, $xmlFile) {
		$file = $this->getPath($this->attrValue($importEl, "file"), $xmlFile);
		if (!file_exists($file)) {
			throw new Exception(
				"Cant find file: ".$this->attrValue($objectEl, "file"));
		}
		$this->addXmlFile($file);
	}

	private function assimilateProperties($propertiesEl, $xmlFile) {
		$files							= $this->attrValue($propertiesEl, "file");
		$addEnvironmentVariables		= $this->attrValue($propertiesEl, "add-env-vars",		"false");
		$environmentVariablesOverride	= $this->attrValue($propertiesEl, "env-vars-override",	"false");
		$dieOnError						= $this->attrValue($propertiesEl, "die-on-error",		"true");

		// check for sub files
		if (!pmvc_hasVal($files)) {
			$files = Array();
			foreach ($propertiesEl->xpath("file") as $fileEl) {
				array_push($files, $this->getPath((string)$fileEl, $xmlFile));
			}

		// single file
		} else if (pmvc_hasVal($files) && $files!==FALSE) {
			$files = Array($this->getPath($files, $xmlFile));

		// nothing!
		} else {
			\pmvc\log\Log::warn("No files specified for <properties> tag");
			$files = Array();
		}
	
		// create PropertyDescriptors
		$fileProps = Array();
		foreach ($files as $file) {
			array_push($fileProps, new PropertyDescriptor(NULL, $file, PropertyDescriptor::TYPE_STRING));
		}

		// add to ObjectDescriptor
		$id = uniqid('', TRUE)."#PropertiesDescriptorPostProcessor";
		$desc = new ObjectDescriptor(
			$id, $id, "pmvc\ioc\PropertiesDescriptorPostProcessor", Array(
				new PropertyDescriptor("addEnvironmentVariables", 		$addEnvironmentVariables=="true", 		PropertyDescriptor::TYPE_BOOLEAN),
				new PropertyDescriptor("environmentVariablesOverride", 	$environmentVariablesOverride=="true", 	PropertyDescriptor::TYPE_BOOLEAN),
				new PropertyDescriptor("dieOnError", 					$dieOnError=="true", 					PropertyDescriptor::TYPE_BOOLEAN),
				new PropertyDescriptor("propertyFiles", 				$fileProps, 							PropertyDescriptor::TYPE_ARRAY)
			));
		parent::addObjectDescriptor($desc);
		return $desc;
	}

	private function assimilateObject($objectEl, $xmlFile) {
		// get constructor args
		$constructorArgs = Array();
		foreach ($objectEl->xpath("constructor-arg") as $ctrEl) {
			array_push($constructorArgs, $this->assimilateProperty($ctrEl, $xmlFile));
		}
		
		// get properties
		$properties = Array();
		foreach ($objectEl->xpath("property") as $propertyEl) {
			array_push($properties, $this->assimilateProperty($propertyEl, $xmlFile));
		}

		// get class data
		$class		= $this->attrValue($objectEl, "class");
		$uniqid		= uniqid('', TRUE)."#".$class;
		
		$id			= $this->attrValue($objectEl, "id", $uniqid);
		$name		= $this->attrValue($objectEl, "name", $uniqid);
		$singleton	= $this->attrValue($objectEl, "singleton", "true");
		$abstract	= $this->attrValue($objectEl, "abstract", "false");
		$parent		= $this->attrValue($objectEl, "parent");
		$lazy		= $this->attrValue($objectEl, "lazy-init", "true");
		$initMethod	= $this->attrValue($objectEl, "init-method");

		// add the descriptor
		$desc = new ObjectDescriptor(
			$id, $name, $class, $properties, $constructorArgs, 
			strtolower($singleton)=="true", strtolower($abstract)=="true",
			$parent, strtolower($lazy)=="true", $initMethod);
		parent::addObjectDescriptor($desc);
		return $desc;
	}

	private function assimilateArray($arrayEl, $xmlFile) {
		$value = Array();
		foreach ($arrayEl->xpath("entry") as $entryEl) {
			array_push($value, $this->assimilateProperty($entryEl, $xmlFile));
		}
		return $value;
	}

	private function assimilateProperty($propertyEl, $xmlFile) {

		// get type name and value
		$type	= $this->attrValue($propertyEl, "type",	PropertyDescriptor::TYPE_STRING);
		$name	= $this->attrValue($propertyEl, "name", $propertyEl->name);
		$value	= $this->attrValue($propertyEl, "value", $propertyEl->value);

		// if we don't have a value try the "ref" element
		if ($value==NULL || !$value) {
			$value = $this->attrValue($propertyEl, "ref", $propertyEl->ref);
			if ($value!=NULL && $value) {
				$type = PropertyDescriptor::TYPE_REF;
			}
		}

		// if it's not a ref lets assimilate arrays and objects
		if ($type!=PropertyDescriptor::TYPE_REF) {

			// is this an array?
			if (!pmvc_hasVal($value) && $propertyEl->array) {
				$value = $this->assimilateArray($propertyEl->array, $xmlFile);
				$type = PropertyDescriptor::TYPE_ARRAY;

			// is this an object?
			} else if (!pmvc_hasVal($value) && $propertyEl->object) {
				$objDesc = $this->assimilateObject($propertyEl->object, $xmlFile);
				$value = $objDesc->id;
				$type = PropertyDescriptor::TYPE_REF;
			}
		}

		// create the property descriptor
		return new PropertyDescriptor($name, $value, $type);
	}

	private function attrValue($element, $name, $default=NULL) {
		return  (pmvc_hasVal((string)$element[$name]))
			? (string)$element[$name] : $default;
	}

	private function getPath($file, $relative) {
		if (file_exists($file)) {
			return $file;
			
		} else if (substr($file, 0, 1)!="/"
			&& file_exists(dirname($relative)."/".$file)) {
			return dirname($relative)."/".$file;
			
		} else if (substr($file, 0, 1)=="/"
			&& file_exists(dirname($relative).$file)) {
			return dirname($relative).$file;
			
		} else {
			return $file;
		}
	}

}

?>